<?php
// +-------------------------------------------------------------------------+
// | PHP Version 5                                                           |
// +-------------------------------------------------------------------------+
// | ImaEngine - web development framework                                   |
// | Copyright (c) 2006-2007 Sardar Yumatov <ja.doma@gmail.com>              |
// +-------------------------------------------------------------------------+
// | This library is free software; you can redistribute it and/or modify it |
// | under the terms of the GNU Lesser General Public License as published   |
// | by the Free Software Foundation; either version 2.1 of the License, or  |
// | (at your option) any later version.                                     |
// |                                                                         |
// | This library is distributed in the hope that it will be useful, but     |
// | WITHOUT ANY WARRANTY; without even the implied warranty of              |
// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                    |
// | See the GNU Lesser General Public License for more details.             |
// |                                                                         |
// | You should have received a copy of the GNU Lesser General Public        |
// | License along with this library; if not, write to the Free Software     |
// | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
// +-------------------------------------------------------------------------+

/**
* Simple yet powerfull templater implementation.
*
* Each template is a single document with multiple blocks. Blocks are just captured
* piece of template, that may be executed many times. Blocks may be nested, however
* this relationship is traceable only to 'source parent', blocks don't contain
* childblocks. This means all blocks are independent and must have unique names
* whithin a single template. Each block may be executed in any place of the template,
* regardless of the capturing position.
*
* Simple list based organization of the blocks allows calling a block by unique name
* without enumerating all of the "parent" blocks. This allows modifying the template
* and introducing new blocks very easy without need of updating the block calls.
*
* Block definition has nothing to do with block evaluation! The template handler may
* introduce calling context and link block tags with different values depending on context.
*
* @author Sardar Yumatov <ja.doma@gmail.com>
* @copyright Copyright &copy; 2006-2007 Sardar Yumatov
* @version v1.0
* @package common.template
* @category Common
*
* @PHP5only
*/

if(!defined('PROCESS')) {
	header('HTTP/1.1 404 Not Found');
	die('NotFound');
}


require_once('lib/GenericExceptions.php');

/**
 * Template handler definition.
 * Class implementing this interface will be asked for each tag construction in template, the output
 * will replace the tag.
 *
 * @author Sardar Yumatov <ja.doma@gmail.com>
 */
interface TemplateDocumentHandler {

	/**
	 * Process given tag.
	 * The tag will be processed in:
	 *  - $action name - the tag action name, by default 'marker'
	 *  - $markers - list of markers in original order, associative keys preserved
	 *
	 * With default parser handler only 'block-start' and 'block-end' actions can not be
	 * processed by this template handler, because they are recognized at compile time to
	 * define/capture template blocks. Other actions don't have predefined meaning, but abstract handlers
	 * may have default implementation for them.
	 *
	 * This method should output everything that will act as replacement for the tag.
	 * If method returns false, then whole output will be discarded.
	 *
	 * @param AbstractTemplateDocumentBlock $block the current block instance being processed
	 * @param string $action the action name (mark, include, process etc)
	 * @param array $markers the list of markers defined by the user
	 * @return boolean true if output should be used, false if output should be discarded
	 */
	public function processTag(AbstractTemplateDocumentBlock $block, $action, $markers);

	/**
	 * Start processing the block/template.
	 *
	 * Because the template is a root block the first call will always be to this method
	 * with $thisblock set to null. Included template will also begin with <tt>startBlock()</tt> call,
	 * but <tt>$thisblock</tt> will contain current block in the parent template where in including
	 * was performed.
	 *
	 * Any otput from the method will be placed before template block output. If method
	 * retuns false, then output will be discarded and blok will not be procesed. This means
	 * the markers inside the block will not be processed and subsequent <tt>endBlock()</tt> method
	 * will not be called.
	 *
	 * @param AbstractTemplateDocumentBlock $thisblock the current block being processed or null if root block of topmost template is started (seed)
	 * @param AbstractTemplateDocumentBlock $openblock the block instance being opened, may be from other template
	 * @return boolean false if block shold be discarded together with output, true otherwise
	 */
	public function startBlock($thisblock, AbstractTemplateDocumentBlock $openblock);

	/**
	 * Stop processing the block/template.
	 *
	 * This method will be called after <tt>startBlock()</tt> and any subsequent <tt>processTag()</tt> calls.
	 * Any output will be placed right after the block output. If method returhs true, then block will be processed
	 * again with leading <tt>startBlock()</tt> call. Otherwise template processor will execute next
	 * template construction.
	 *
	 * @param AbstractTemplateDocumentBlock $block the block instance being closed (current block)
	 * @return boolean true if this block should be executed again, false otherwise
	 */
	public function endBlock(AbstractTemplateDocumentBlock $block);
}

/**
 * Template loader/provider interface.
 *
 * This interface will be used by actions like 'include' to obtain a template by name from the provider of
 * current template. This is transparent to the user which uses template name and optional parameters without
 * knowing anything about how such template may be obtained. Instance of this interface implements some naming
 * scheme where name of the template is used among with other optional parameters.
 *
 * @author Sardar Yumatov <ja.doma@gmail.com>
 */
interface TemplateDocumentProvider {

	/**
	 * Load and return template by $name.
	 *
	 * Method should load $name template if it is not loaded before. The $params may contain
	 * optional parameters like skin or locale used to override defaults. All providers must
	 * be able to load template by $name only using current configuration.
	 *
	 * <b>Warning:</b> provider must not accept any $params with nummeric keys! Usually $params will
	 * contain much more info than parameters for provider (list of markers from the tag).
	 *
	 * @throws NotFoundException if template named $name is not found
	 * @throws RuntimeException on any other error (syntax etc)
	 * @param string $name name of the template, any specific rules are allowed
	 * @param array $params associative array with optional parameters
	 * @return AbstractTemplateDocument loaded document
	 */
	public function getTemplate($name, $params=null);

	/**
	 * Returns current provider configuration.
	 * The method returns options that where set when creating provider, from <tt>Configuration</tt> or
	 * hardcoded defaults. The provider always (merges user_given_options + this_options + defaults)
	 * when loading a template.
	 * @return array configuration as list of option=>value pairs that will be used by <tt>getTemplate()</tt> as defaults.
	 */
	public function getConfiguration();

	/**
	 * Returns true if given $provider is recognized and is equaly configured.
	 * Equaly configured providers will return the same template if no other options are provided
	 * while obtaing template.
	 * @param TemplateDocumentProvider $provider another provider to compare
	 * @return boolean true if both providers are of the same type and equaly configured
	 */
	public function isEqual(TemplateDocumentProvider $provider);
}

/**
 * Output filter interface.
 *
 * The filter object may implement one or more filters that are able to transform
 * any output to desired format. Think about 'uppercase' or 'date' which converts
 * text to uppercase letters or format given timestamp to extended localized date format.
 *
 * Filters are shared accorss all templaters. The filter needs to be registerd first.
 *
 * @author Sardar Yumatov <ja.doma@gmail.com>
 */
interface TemplateOutputFilter {

	/**
	 * Filter/transform given input buffer.
	 * Method shall transform given text and echo the results.
	 *
	 * @param string $name name of the filter, so one object will be capable to handle many filters
	 * @param string $input text input to process
	 * @param array $params any parameters set in tag (associative keys and order preserved) or null
	 * @return boolean if false, then original text will be returned/used instead of captured one
	 */
	public function filterOutput($name, $input, $params);
}

/**
 * Basic wrapping handler implementation.
 * This class delegates all block events to the wrapped handler.
 * @author Sardar Yumatov <ja.doma@gmail.com>
 */
abstract class AbstractTemplateDocumentHandler implements TemplateDocumentHandler {

	/** Wrapped handler
	 * @var TemplateDocumentHandler */
	protected $parentHandler;

	/** Create new wrapper.
	 * @param TemplateDocumentHandler $handler the parent handler or null if none */
	public function __construct($handler=null) {
		$this->parentHandler = $handler;
	}

	/**
	 * Method delegates call to wrapped handler or returns false if none is set.
	 * @param AbstractTemplateDocumentBlock $block current executing block
	 * @param string $action current tag action
	 * @param array $markers list of markers in original order with associative keys preserved
	 * @return boolean false if parent handler is not set, otherwise return from parent hanler
	 */
	public function processTag(AbstractTemplateDocumentBlock $block, $action, $markers) {
		return is_null($this->parentHandler)? false: $this->parentHandler->processTag($block, $action, $markers);
	}

	/**
	 * Start processing the block/template.
	 * Method delegates call to the wrapped handler or returns true if none is set.
	 * @param AbstractTemplateDocumentBlock $thisblock the curren block being processed.
	 * @param AbstractTemplateDocumentBlock $openblock the block instance being opened
	 * @return boolean returned by wrapped handler value or true
	 */
	public function startBlock($thisblock, AbstractTemplateDocumentBlock $openblock) {
		return is_null($this->parentHandler)? true: $this->parentHandler->startBlock($thisblock, $openblock);
	}

	/**
	 * Stop processing the block/template
	 * Method delegates call to the wrapped handler or returns false if none is set.
	 * @param AbstractTemplateDocumentBlock $block the block sinstance being closed
	 * @return boolean returned by wrapped handler value or false
	 */
	public function endBlock(AbstractTemplateDocumentBlock $block) {
		return is_null($this->parentHandler)? false: $this->parentHandler->endBlock($block);
	}
}

/**
 * This class represents a template.
 * Abstract implementation of the functionality common to all templaters.
 * @author Sardar Yumatov <ja.doma@gmail.com>
 */
abstract class AbstractTemplateDocument {

	/**
	 * The filters registry - maps filter_name => handler_object
	 * If handler is a string, then this string is used as global function name (ucfirst etc).
	 * Shared across all templates.
	 * @var array of TemplateOutputFilter or string
	 */
	protected static $filters = array( //default filters
		'uppercase'=>'strtoupper',
		'lowercase'=>'strtolower',
		'capitalize'=>'strtolower',
		'capfirst'=>'ucfirst'
	);

	/**
	 * The name of the template.
	 * @var string
	 */
	private $name;

	/**
	 * This template root block.
	 * @var AbstractTemplateDocumentBlock
	 */
	protected $rootblock;

	/**
	 * All other blocks as name => instance
	 * @var array of AbstractTemplateDocumentBlock
	 */
	protected $blocks;

	/** Provder, may be set by any name scheme
	 * @var TemplateDocumentProvider
	 */
	protected $provider;

	/**
	 * Register filter.
	 * The filter will be available to all templates. Given $filter must be
	 * a <tt>TemplateOutputFilter</tt> object or a string. In case of string
	 * the $filter is used as a name of build in PHP function like 'strtoupper'.
	 *
	 * @param string $name name of the filter, will be trim()'ed
	 * @param TemplateOutputFilter|string $filter the filter handler, either object or a function name
	 * @return TemplateOutputFilter|string old handler or null if filter is new
	 */
	public static function setFilter($name, $filter) {
		if(($name = trim($name)) == '') throw new InvalidArgumentException('Name of the filter may not be empty!');
		if(!is_string($filter) && !($filter instanceof TemplateOutputFilter)) throw new InvalidArgumentException("Given filter handler for name {$name} is not a TemplateOutputFilter object nor a string!");
		$ret = isset(self::$filters[$name])? self::$filters[$name]: null;
		self::$filters[$name] = $filter;
		return $ret;
	}

	/**
	 * Returns filter handler.
	 * This handler may be used by document handler to process any data, however
	 * <tt>executeFilter()</tt> may be simpler to use.
	 * @see executeFilter()
	 * @param string name of the filter
	 * @return TemplateOutputFilter|string the filter or null if not defined
	 */
	public static function getFilter($name) {
		return isset(self::$filters[$name])? self::$filters[$name]: null;
	}

	/**
	 * Execute a filter and return the output.
	 * This method is used by template blocks to execute filters one by one.
	 * If filter is not defined, then $input will be returned as is and notice will be generated.
	 * If filter handler is a string, then only single $input will be passed to the function.
	 *
	 * @param string $name name of the filter
	 * @param string $input the text input
	 * @param array $params filter parameters as defined in the tag
	 * @return string filtered $input
	 */
	public static function executeFilter($name, $input, $params) {
		if(is_null($fil = self::getFilter($name))) {
			trigger_error("Filter named '{$name}' is not registered!", E_USER_NOTICE);
			return $input; //just ignore filter
		}
		if($fil instanceof TemplateOutputFilter) {
			ob_start();
			if($fil->filterOutput($name, $input, $params)) return ob_get_clean();
			ob_end_clean();
			return $input;
		} else {
			if(!function_exists($fil)) return $input;
			return call_user_func($fil, $input);
		}
	}

	/**
	 * Init this template document.
	 * Concrete implementations may may load and parse a template, use data from cache or load compiled
	 * templates. This constructor simply initializes the name only.
	 *
	 * Concrete implementations must initialize $rootblock and $blocks variables of this class,
	 * providing necessary output data.
	 *
	 * @param string $name name of the template
	 * @param string|FilePath $path absolute path to template source file
	 */
	protected function __construct($name) {
		$this->name = $name;
	}

	/**
	 * Returns template name.
	 * @return string
	 */
	public function getName() {
		return $this->name;
	}

	/**
	 * Returns template provider that has loaded this template instance.
	 * @return TemplateDocumentProvider provider that may be used to implement 'include' action or null if this template is independent
	 */
	public function getProvider() {
		return $this->provider;
	}

	/**
	 * Set template provider.
	 * This method may be called by naming scheme (provider) before returning template back.
	 * This method may be obscure, but it is much better than trying to wrap everything.
	 * @param TemplateDocumentProvider $provider
	 */
	public function setProvider(TemplateDocumentProvider $provider) {
		$this->provider = $provider;
	}

//==========- Blocks -=============
	/**
	 * Returns root block (template itself) of this template.
	 * @return AbstractTemplateDocumentBlock
	 */
	public function getRootBlock() {
		return $this->rootblock;
	}

	/**
	 * Returns list of all defined child block names.
	 * @return array of strings
	 */
	public function listBlocks() {
		return array_keys($this->blocks);
	}

	/**
	 * Returns named block from template.
	 * @param string $name name of the block
	 * @return AbstractTemplateDocumentBlock one of defined blocks or null if not defined
	 */
	public function getBlock($name) {
		return isset($this->blocks[$name])? $this->blocks[$name]: null;
	}

	/**
	 * Execute block.
	 * This is a macro-like method that implements standart block handling bechavior.
	 * @param TemplateDocumentHandler $handler the block/template handler
	 * @param AbstractTemplateDocumentBlock $thisblock current processing block, null for the starting root block of topmost template
	 * @param AbstractTemplateDocumentBlock $openblock the block being opened
	 * @return boolean always returns true
	 */
	public static function executeBlockObject(TemplateDocumentHandler $handler, $thisblock, AbstractTemplateDocumentBlock $openblock) {
		do {
			ob_start();
			if(!$handler->startBlock($thisblock, $openblock)) {
				ob_end_clean();
				return true;
			} else ob_end_flush();
			$openblock->processOutput($handler);
		} while($handler->endBlock($openblock));
		return true;
	}

	/**
	 * Execute block by name.
	 * @param TemplateDocumentHandler $handler the template/block handler
	 * @param AbstractTemplateDocumentBlock $thisblock current block being processed
	 * @param string $name name of the block to process
	 * @return boolean true if block is found and processed, false otherwise
	 */
	public function executeBlock(TemplateDocumentHandler $handler, AbstractTemplateDocumentBlock $thisblock, $name) {
		if(is_null($bl = $this->getBlock($name))) return false;
		return self::executeBlockObject($handler, $thisblock, $bl);
	}

	/**
	 * Execute this template against given handler.
	 * @param TemplateDocumentHandler $handler the template handler
	 * @return boolean always returns true
	 */
	public function processOutput(TemplateDocumentHandler $handler) {
		return self::executeBlockObject($handler, null, $this->getRootBlock());
	}
}


/**
 * The abstract template block.
 * This class represents a single template block - piece of template text that may be
 * processed many times.
 *
 * @author Sardar Yumatov <ja.doma@gmail.com>
 */
abstract class AbstractTemplateDocumentBlock {

	/**
	 * Filters defined with the block definition.
	 * Filters from this set will be passed prior any other filters to the whole block output, null if not used.
	 * Associative array in specific order, where key is the filter name and value is the array with arguments.
	 * @var array of array (name => (arguments | null))
	 */
	protected $pre_filter_chain = null;

	/**
	 * Name of the block, null if this is a template file (root block)
	 * @var string
	 */
	private $name;

	/**
	 * Parent block. Null if this is a root block.
	 * @var AbstractTemplateDocumentBlock
	 */
	private $parent;

	/**
	 * AbstractTemplateDocument instance that has loaded this block.
	 * @var AbstractTemplateDocument
	 */
	protected $template;

//==========================- Instance specific API -=======================================

	/**
	 * Initialize private class members.
	 * If $template is null, then this block is probably used only to process some piece of text
	 * using standart parser (a very rare case)
	 *
	 * Concrete implementation should initialize <tt>$pre_filter_chain</tt> if filters where used
	 * together with this block definition.
	 *
	 * @param string $name name of the block, null for root block
	 * @param AbstractTemplateDocumentBlock $parent parent block or null if this is the root block
	 * @param AbstractTemplateDocument $template the template document instance, that is loading this block
	 */
	protected function __construct($name, $parent, $template) {
		$this->name = $name;
		$this->parent = $parent;
		$this->template = $template;
	}

	/**
	 * Returns name of this block or null if this is a root block.
	 * @return string
	 */
	public function getName() {
		return $this->name;
	}

	/**
	 * Returns template instance, that holds all other blocks and info.
	 * @return AbstractTemplateDocument may be null if this block is not associated with any template (a very rare case)
	 */
	public function getTemplateDocument() {
		return $this->template;
	}

	/**
	 * Returns parent block.
	 * @return AbstractTemplateDocumentBlock parent block or null if this is a root block
	 */
	public function getParent() {
		return $this->parent;
	}

	/**
	 * Returns list of predefined filters.
	 * @return array of array [name => (arguments | null)]
	 */
	public function getFilters() {
		return $this->pre_filter_chain;
	}

	/**
	 * Execute block by name.
	 * This method uses api of parent template document to execute the block by $name with this block as current one.
	 * @param TemplateDocumentHandler $handler the template/block handler
	 * @param string $name name of the block to process
	 * @return boolean true if block is found and processed, false otherwise
	 */
	public function executeBlock(TemplateDocumentHandler $handler, $name) {
		return $this->getTemplateDocument()->executeBlock($handler, $this, $name);
	}

	/**
	 * Start processing the block.
	 * Method calls <tt>generateOutput()</tt> of the concrete implementation to generate text.
	 * If <tt>$pre_filter_chain</tt> was defined, then output will be captured and filtered
	 * by the filter chain.
	 * @param TemplateDocumentHandler $handler processor of template structures
	 * @return boolean always returns true
	 */
	public function processOutput(TemplateDocumentHandler $handler) {
		if(is_array($this->pre_filter_chain) && !empty($this->pre_filter_chain)) {
			ob_start();
			$this->generateOutput($handler);
			$cont = ob_get_clean();
			foreach ($this->pre_filter_chain as $name=>$params)
				$cont = AbstractTemplateDocument::executeFilter($name, $cont, $params);
			echo $cont;
		} else $this->generateOutput($handler);
		return true;
	}

	/**
	 * Generate the text output of this block.
	 * All found tags should be send to the $handler.
	 * @param TemplateDocumentHandler $handler template handler
	 * @return void the text content should be send to output buffer
	 */
	protected abstract function generateOutput(TemplateDocumentHandler $handler);
}


/*
{% var %} {%marker: var%}
{%mark: var gsggdfklj | processor | processor %}

{%include: "template name" %}

{%block-start: name %}
	....
{%block-end: name %}

{%process: name %} //process the block
{%process: name | processor | processor %}
*/
?>